package Square;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;

import Border.Border;
import Border.BorderType;
import Border.Direction;
import Border.exceptions.IllegalBorderTypeException;
import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Raw;

/**
 * 
 * A class of squares for a RPG gameboard
 * 
 * @author 	Koen Certyn & Michiel Meersmans 
 * 
 * @invar	The square temperature will always be a value between the minimum and maximum temperature
 * 			| Square.isValidTemperature(getTemperature(), getMinTemperature(), getMaxTemperature())
 * 
 * @invar 	The square will never have more then 3 doors
 * 			| countAmountOfDoors() <= 3 
 * 
 * @invar	A square's amount of borders will always be a value between 1 and 6
 * 			| countAmountOfBorders() >= 1 && countAmountOfBorders() <= 6
 */

public abstract class Square {

	final static double EPSILON = 0.0000001;
	
	/**
	 * A variable containing the amount of damage a creature will get per temperature unit 
	 * when the temperature goes above the heat damage limit
	 */
	private static double heatDamagePerCelcius = (1.0 / 15.0);
	
	/**
	 * A variable containing the minimum temperature that needs to be reached in order to deal heat damage
	 */
	private static double heatDamageLimit = 35;
	
	/**
	 * Returns the heat damage a creature will get per temperature unit above the limit
	 * 
	 */
	@Basic
	public static double getHeatDamagePerCelcius()
	{
		return heatDamagePerCelcius;
	}
	
	/**
	 * Sets the amount of heat damage a creature will get per temperature unit
	 * 
	 * @param 	value
	 * 			The amount of heat damage a creature will get per temperature unit
	 * @post	The amount of heat damage a creature will get per temperature unit equals the newly given value
	 * 			| this.getHeatDamagePerCelcius == value
	 */
	public static void setHeatDamagePerCelcius(double value)
	{
		heatDamagePerCelcius = value;
	}
	
	/**
	 * 
	 * Returns the minimum temperature a square needs to deal heat damage
	 * 
	 */
	@Basic
	public static double getHeatDamageLimit()
	{
		return heatDamageLimit;
	}
	
	
	/**
	 * Sets the minimum temperature a square needs to give heat damage
	 * 
	 * @param 	value
	 * 			The minimum temperature a square needs to give heat damage
	 * @post	The minimum temperature a square needs to give heat damage equals the newly given value
	 * 			| this.getHeatDamageLimit() == value
	 */
	public static void setHeatDamageLimit(double value)
	{
		heatDamageLimit = value;
	}
	
	
	/**
	 * 
	 * Creates a new square with a given temperature, humidity percentage and a number of border directions
	 * 
	 * @effect	The borders are initialized on border type empty
	 * 			| initBorders()
	 * 			
	 */
	public Square() {
		initBorders();		
	}
	
	
	/**
	 * Return the current temperature of this square in celcius.
	 * 
	 */
	@Basic
	public abstract double getTemperature();
	

	/**
	 * Return the current temperature of this square in kelvin.
	 * 
	 * @return	The temperature of this square in kelvin
	 * 			| result == getTemperature() + 273.15
	 */
	public double getTemperatureInKelvin()
	{
		return getTemperature() + 273.15;
	}
	
	/**
	 * Return the current temperature of this square in fahrenheit.
	 * 
	 * @return	The temperature of this square in fahrenheit
	 * 			| result == (getTemperature() * 9 / 5) + 32
	 */
	public double getTemperatureInFahrenheit()
	{
		return (getTemperature() * 9 / 5) + 32;
	}
	
	/**
	 * Return the Cold Damage a creature will get in this square.
	 * 
	 * @return	The damage a creature will get from the cold in this square.
	 * 			| if (getTemperature() > -5) then
	 * 			|		result == 0
	 * 			| else
	 * 			|		result == floor( abs( (getTemperature() + 5) / 10 ) ) )
	 * 
	 */
	public int getColdDamage()
	{
		if(getTemperature() > -5)
			return 0;
		else
			return (int) Math.floor( Math.abs( (getTemperature() + 5) / 10.0 ) );
	}
	
	/**
	 * Returns the damage a creature will get from the heat in this square
	 * 
	 * @return	The damage a creature will get from the heat in this square
	 * 			| if(getTemperature() < getHeatDamageLimit()) then
	 * 			|	result == 0
	 * 			| else
	 * 			| 	result == floor( (getTemperature() - getHeatDamageLimit()) * getHeatDamagePerCelcius())
	 */
	public int getHeatDamage()
	{
		if(getTemperature() < getHeatDamageLimit())
			return 0;
		else
			return (int) Math.floor( (getTemperature() - getHeatDamageLimit()) * getHeatDamagePerCelcius());
	}
		
	/**
	 * Returns the humidity of this square.
	 * 
	 */
	public abstract BigDecimal getHumidity();
		
	/**
	 * Returns the damage metal objects will get by rusting in this square.
	 * 
	 * @return	The rust damage a metal object will get from this square.
	 * 			| if (getHumidity() < 30) then
	 *			|		result == 0
	 *			| else
	 *		    | 		result == floor((getHumidity() - 30) / 7);
	 * 
	 */
	public int getRustDamage()
	{
		if(getHumidity().compareTo( new BigDecimal( 30 ) ) < 0)
			return 0;
		else
		{
			BigDecimal rustDamage = getHumidity().subtract( new BigDecimal(30) ).divide(new BigDecimal(7));
			return (int) Math.floor( rustDamage.doubleValue() );
		}
	}
	
	/**
	 * A variable containing whether the floor of this square is slippery or not
	 */
	private boolean isSlipperyMaterial = false;
	
	/**
	 * Returns true if the material is slippery, returns false if it isn't
	 */
	@Basic
	public boolean isSlipperMaterial()
	{
		return this.isSlipperyMaterial;
	}
	
	
	/**
	 * 
	 * @param 	value
	 * 			Value that equals true if the material is slippery, false if it isn't
	 * 
	 * @post	The material will be made out of slippery material or non-slippery material depending 
	 * 			on the given value
	 * 			| new.isSlipperyMaterial() == value
	 *  
	 */
	public void makeSlipperyMaterial(boolean value)
	{
		this.isSlipperyMaterial = value;
	}
	
	
	/**
	 * Returns true if the square is slippery
	 * 
	 * @return	True if the square is slippery:
	 * 				- The humidity is 100% whilst the temperature is positive
	 * 				- The humidity is higher then 10% whilst the temperature is below zero
	 * 				- The floor is made out of a slippery material
	 * 			False in all other cases
	 * 			| if( ((100 - getHumidity()) < EPSILON) && (getTemperature() > 0) ) then
	 *			| 	result == true
	 *			| else if( (getTemperature() < 0) && getHumidity() > 10 ) then
	 *			| 	result == true
	 *			| else if (isSlipperMaterial())
	 *			| 	result == true
	 *			| else
	 *			| 	result == false
	 * 
	 */
	public boolean isSlippery()
	{
		if( ( getHumidity().compareTo( new BigDecimal(100) ) == 0 ) && (getTemperature() > 0) )
			return true;
		else if( (getTemperature() < 0) && getHumidity().compareTo( new BigDecimal(10) ) > 0 )
			return true;
		else if (isSlipperMaterial())
			return true;
		
		return false;	
	}
	
	/**
	 * Returns Inhabitability degree
	 * 
	 * @returns	The inhabitability degree of the square
	 * 			| (-sqrt(getHeatDamage()^3) / sqrt(101 - getHumidity())) - sqrt(getColdDamage())
	 */
	public double getInhabitabilityDegree()
	{
		double inhabitabilityDegree;		
		inhabitabilityDegree = Math.sqrt(Math.pow(getHeatDamage(), 3)) / Math.sqrt(new BigDecimal(101).subtract(getHumidity()).doubleValue());
		inhabitabilityDegree = -inhabitabilityDegree - Math.sqrt(getColdDamage());
		return inhabitabilityDegree;
	}
	
	/**
	 * A hash map with a direction as key and a border object as value
	 */
	private HashMap<Direction, Border> borders = new HashMap<Direction, Border>(6);
	
	/**
	 * A method to check if the border type can be changed to a given type on a given direction for this square
	 * 
	 * @param 	type
	 * 			The border type which you want the square to change to
	 * 
	 * @param 	direction
	 * 			The direction of the border that will be changed
	 * 
	 * @return	True if the border at given direction can be changed to the given type, false if it can't
	 */
	abstract public boolean canHaveAsBorderType(BorderType type, Direction direction);
	
	public Direction getDirectionFromBorder(Border border)
	{
		for(Direction direction : Direction.values())
		{
			if(this.getBorderAt(direction) == border)
				return direction;
		}
		
		// Border hashmap will always be initialized, so if the for loop doesn't return a direction something went wrong in
		// the initialization => assert false;
		assert false;
		return null;
	}
	
	/**
	 * A method that initializes the borders to EMPTY
	 * 
	 * @post	All the border's types are set to EMPTY
	 * 			| for each direction in Border.Direction
	 * 			| 	new.getBorderAt(direction).getBorderType() == Border.BorderType.EMPTY
	 */
	@Raw
	private void initBorders()
	{
		for(Direction direction : Direction.values())
		{
			Border border = new Border(this, BorderType.EMPTY);
			this.addBorder(direction, border);
		}
	}
	
	/**
	 * 
	 * Returns whether the square has a border at a given direction or not
	 * 
	 * @param 	direction
	 * 			The direction to check a border.
	 * @pre		The given direction has to be effective
	 * 			| direction != null
	 * @return 	False if the square has a border with bordertype EMPTY at given direction, true in all other cases
	 *			| if(getBorderAt(direction).getBorderType() == BorderType.EMPTY)
	 *			|		result == false
	 *			|	else
     *			|		result == true
	 * 
	 */
	public boolean hasBorderAt(Direction direction)
	{
		assert direction != null;
		if(getBorderAt(direction).getBorderType() == BorderType.EMPTY)
			return false;
		else
			return true;
	}
	
	/**
	 * Returns a border object from a given direction
	 * @param 	direction
	 * 			The direction to get the border from.
	 * @pre		The given direction has to be effective
	 * 			| direction != null
	 * @return	A border object from a given direction
	 */
	@Basic
	public Border getBorderAt(Direction direction)
	{
		assert direction != null;
		return borders.get(direction);
	}
	
	
	
	/**
	 * 
	 * Creates a border for at given number
	 * 
	 * @param 	border
	 * 			The border object that needs to added to this square
	 * @pre		The given border must be effective
	 * 			| border != null
	 * @pre		The given direction must be effective
	 * 			| direction != null
	 * @pre 	The given border must reference this square as one of its squares
	 * 			| border.getFirstSquare() == this || border.getSecondSquare() == this
	 * @pre		The border can't be a door if the square already has 3 doors
	 * 			| (border.getBorderType() != Border.BorderType.OPEN_DOOR && border.getBorderType() != Border.BorderType.CLOSED_DOOR) || countAmountOfDoors() < 3;
	 * @post	The border is added to the square
	 * 			| new.getBorderAt( direction ) == border
	 * @note	If the square already has a border at the given direction that border will be overwritten
	 * 
	 */
	private void addBorder(Direction direction, Border border)
	{
		assert border != null;
		assert direction != null;
		assert border.getFirstSquare() == this || border.getSecondSquare() == this;
		assert (border.getBorderType() != BorderType.OPEN_DOOR && border.getBorderType() != BorderType.CLOSED_DOOR) || countAmountOfDoors() < 3;
		if( borders.containsKey( direction ) )
			borders.remove(direction);
		borders.put(direction, border);
	}
		
	/**
	 * Returns the amount of doors in this square
	 * 
	 * @return	The amount of doors in the square
	 * 			| amount = 0
	 * 			| for each square in getAllBorders()
	 * 			| 	if( square.isDoor() )
	 *   		|		amount++
	 *   		| result == amount
	 */
	public int countAmountOfDoors()
	{
		int amount = 0;
		Collection<Border> collection = borders.values();
		 
	    Iterator<Border> i = collection.iterator();

	    while(i.hasNext())
	    {
	    	Border element = i.next();
	    	if( element.isDoor() )
	    		amount++;
	    }

		return amount;
	}
	
	/**
	 * Returns the amount of borders in this square
	 * 
	 * @return	The amount of borders in the square
	 * 			| amount = 0
	 * 			| for each square in getAllBorders()
	 * 			| 	if( square.getBorderType() != Border.BorderType.EMPTY )
	 *   		|		amount++
	 *   		| result == amount
	 */
	public int countAmountOfBorders()
	{
		int amount = 0;
		Collection<Border> collection = borders.values();
		 
	    Iterator<Border> i = collection.iterator();

	    while(i.hasNext())
	      if( i.next().getBorderType() != BorderType.EMPTY )
	    	  amount++;

		return amount;
	}
	
	
	/**
	 * 
	 * Merges the borders of 2 squares on a given direction
	 * 
	 * @param 	other
	 * 			The other square to merge borders with
	 * @param 	direction
	 * 			The direction to merge borders on
	 * @post	The border for both squares will be set to the most closed border of the two
	 * 			| Border border = this.getBorderAt(direction).getMostClosedBorder(other.getBorderAt(direction.getOpposite()))
	 *			| border.setFirstSquare(this)
	 *			| border.setSecondSquare(other)
	 *			| this.addBorder(direction, border)
	 *			| other.addBorder(direction.getOpposite(), border)
	 * @throws	IllegalBorderTypeException
	 * 			One of the borders can't have the new border's type as border type
	 * 			| !this.canHaveAsBorderType(border.getBorderType(), direction) || !other.canHaveAsBorderType(border.getBorderType(), direction.getOpposite())
	 */
	public void mergeBorders( Square other, Direction direction) throws IllegalBorderTypeException
	{
		Border border = this.getBorderAt(direction).getMostClosedBorder(other.getBorderAt(direction.getOpposite()));
		
		if(!this.canHaveAsBorderType(border.getBorderType(), direction))
			throw new IllegalBorderTypeException(this, border.getBorderType(), direction);
		if(!other.canHaveAsBorderType(border.getBorderType(), direction.getOpposite()))
			throw new IllegalBorderTypeException(other, border.getBorderType(), direction.getOpposite());
		
		border.setFirstSquare(this);
		border.setSecondSquare(other);
		this.addBorder(direction, border);
		other.addBorder(direction.getOpposite(), border);
		
		
	}
		
	
}
